#
# Copyright (c) Matt Hawley, http://hgtfshook.codeplex.com
#
# This software may be used and distributed under the
# GNU General Public License version 2 (GPLv2)
#
'''
hooks for integrating with Team Foundation Server work item tracking

This hook integrates with Team Foundation Server work item tracking
that adds comments and optionally marks the work item as fixed when
bugs related to a Team Foundation Server project are referred to
in changeset commit messages.

The extension is configured through a configuration section. The
following keys are recognized in the [tfs] section:

host
    The Team Foundation Server host. This should be the url that
    you connect to. ie, http://myserver:8080 or https://myserver

user
    The username used to authenticate with the Team
    Foundation Server. If not specified, it will attempt to use
    the current executing user's credentials.

password
    The password used to authenticate with the Team
    Foundation Server. If not specified, it will attempt to use
    the current executing user's credentials.

resolve (Currently Disabled)
    Optional. This is a boolean value that will mark the work
    item as fixed when the comment is added to the bug.

regexp
    Optional. This is the regex that is used to extract work item
    ids that should be updated when the hook runs. It must contain
    only one capturing "()" group that surrounds the bug ids.

    Default: (?is)Work ?Items?: (.+)$

template
    Optional. Template to use when formatting the comment added
    to the work item. This will override the default template
    used. Keywords include the usual Mercurial keywords.

    Default: Fixed in revision {node|short}.

The [tfs.columns] section is used to specify additional columns
that are also written to when updating a work item template.

Note, if commenting on items against CodePlex, the following is required

    CodePlex.UpdatedBy = user_cp

The [tfs.computedColumns] section is used to specify additional columns
that are computed by the server when updating a work item template. These do not
need a value.

Note, if commenting on items against CodePlex, the following is required

    CodePlex.UpdatedBy =

Example Configuration:

This is an example configuration used to connect to a Team Foundation
Server and update it's work items.

    [tfs]
    host = http://myserver:8080
    user = DOMAIN\user
    password = myPassword

    [tfs.columns]
    CodePlex.UpdatedBy = user_cp

    [tfs.computedColumns]
    CodePlex.UpdatedBy =
'''

from mercurial.i18n import _
from mercurial.node import short
from mercurial import cmdutil, templater, util
from ntlm import HTTPNtlmAuthHandler
from xml.sax.saxutils import escape
import xml.etree.ElementTree as et
import re, httplib, uuid, urllib2, urlparse

class SoapService:
    NS_SOAP_ENV = "{http://schemas.xmlsoap.org/soap/envelope/}"
    NS_XSI = "{http://www.w3.org/2001/XMLSchema-instance}"
    NS_XSD = "{http://www.w3.org/2001/XMLSchema}"

    def __init__(self, url, username, password):        
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
        self.host = netloc
        self.url = '%s://%s%s/WorkItemTracking/v1.0/ClientService.asmx' % (scheme, netloc, path)        
        self.username = username
        self.password = password

    def _appendElement(self, name, value, parent):
        element = et.SubElement(parent, name)
        element.text = value

    def _appendNullElement(self, name, parent):
        element = et.Element(name, {SoapService.NS_XSI + 'nil': 'true'})
        parent.append(element)

    def _buildHeader(self, envelope):
        header = self._getHeader()
        if not header:
            return
        envHeader = et.SubElement(envelope, SoapService.NS_SOAP_ENV + 'Header')
        envHeader.append(header)

    def _buildMessage(self, bodyMessage):
        envelope = et.Element(SoapService.NS_SOAP_ENV + 'Envelope')
        self._buildHeader(envelope)
        body = et.SubElement(envelope, SoapService.NS_SOAP_ENV + 'Body')
        body.append(bodyMessage)
        return envelope

    def _send(self, action, body):
        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
        passman.add_password(None, self.url, self.username, self.password)
        proxy = urllib2.ProxyHandler({})
        ntlm = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(passman)

        opener = urllib2.build_opener(proxy, ntlm)
        urllib2.install_opener(opener)

        message = self._buildMessage(body)
        xmlMessage = et.tostring(message)

        headers = {'Host':self.host,
                   'Content-Type':'text/xml; charset=utf-8',
                   'Content-Length':len(xmlMessage),
                   'SOAPAction':action}

        request = urllib2.Request(self.url, xmlMessage, headers)
        response = urllib2.urlopen(request)
        output = response.read()
        if response.fp.status == 200:
            return et.XML(output)

        raise urllib2.HTTPError(response.geturl(), response.fp.status, response.fp.reason, None, response.fp)

    def _getHeader(self):
        return None

class TfsClientService(SoapService):
    WIT_URL = "http://schemas.microsoft.com/TeamFoundation/2005/06/WorkItemTracking/ClientServices/03"
    NS_WIT = '{%s}' % WIT_URL
    _default_computedColumns = {'System.RevisedDate':None, 'System.ChangedDate':None, 'System.PersonId':None}

    def __init__(self, url, username, password, columns, computedColumns):
        SoapService.__init__(self, url, username, password)

        self.columns = {}
        if columns:
            self.columns.update(columns)
        
        self.computedColumns = TfsClientService._default_computedColumns
        if computedColumns:
            self.computedColumns.update(computedColumns)

    def _buildComputedColumns(self, parent):
        cols = et.SubElement(parent, 'ComputedColumns')
        for name, value in self.computedColumns.iteritems():
            et.SubElement(cols, 'ComputedColumn', {'Column':name})

    def _buildColumns(self, parent):
        cols = et.SubElement(parent, 'Columns')
        for name, value in self.columns.iteritems():
            col = et.SubElement(cols, 'Column', {'Column':name})
            et.SubElement(col, 'Value').text = value

    def _buildElement(self, method):
        return et.Element(method, {'xmlns': TfsClientService.WIT_URL})

    def _getAction(self, method):
        return '"%s/%s"' % (TfsClientService.WIT_URL, method)        

    def _getHeader(self):
        requestHeader = self._buildElement('RequestHeader')
        self._appendElement('Id', 'uuid:%s' % uuid.uuid4(), requestHeader)
        return requestHeader

    def _getColumns(self, table):
        columnData = {}
        columns = table.findall(str.format('./{0}columns//{0}c', TfsClientService.NS_WIT))
        values = table.findall(str.format('./{0}rows/{0}r//{0}f', TfsClientService.NS_WIT))

        if len(values) == 0:
            return None

        i = 0
        for field in values:
            fieldIndex = field.get('k', None)
            if fieldIndex:
                for j in range(i, int(fieldIndex)):
                    key = columns[j].find('./%sn' % TfsClientService.NS_WIT).text
                    columnData[key] = None
                index = int(fieldIndex)
            else:
                key = columns[i].find('./%sn' % TfsClientService.NS_WIT).text
                columnData[key] = field.text
            i += 1
            
        return columnData

    def getWorkItem(self, id):
        method = 'GetWorkItem'
        body = self._buildElement(method)
        self._appendElement('workItemId', str(id), body)
        self._appendElement('revisionId', '0', body)
        self._appendElement('minimumRevisionId', '0', body)
        self._appendNullElement('asOfDate', body)
        self._appendElement('useMaster', "true", body)
        response = self._send(self._getAction(method), body)

        tables = response.findall(str.format('./{0}Body/{1}GetWorkItemResponse/{1}workItem//{1}table', SoapService.NS_SOAP_ENV, TfsClientService.NS_WIT))
        for t in tables:
            if t.get('name') == 'WorkItemInfo':
                return self._getColumns(t)
        return None

    def addWorkItemComment(self, id, rev, comment):
        method = 'Update'
        body = self._buildElement(method)
        package = et.SubElement(body, 'package')
        subPackage = et.SubElement(package, 'Package', {'xmlns':''})
        update = et.SubElement(subPackage, 'UpdateWorkItem', {'ObjectType':'WorkItem', 'WorkItemID':str(id), 'Revision':str(rev)})
        et.SubElement(update, 'InsertText', {'FieldName':'System.History', 'FieldDisplayName':'History'}).text = comment
        self._buildComputedColumns(update)
        self._buildColumns(update)

        self._send(self._getAction(method), body)

class TfsResolver(object):
    _default_regex = r'(?is)Work ?Items?: (.+)$'
    
    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo

        self.resolve = self.ui.config('tfs', 'resolve', False)
        self.template = self.ui.config('tfs', 'template', 'Fixed in revision {node|short}')

        regex = self.ui.config('tfs', 'regexp', TfsResolver._default_regex)
        self.bugRegex = re.compile(regex)

        columns = self.ui.configitems('tfs.columns')
        computedColumns = self.ui.configitems('tfs.computedColumns')

        host = self.ui.config('tfs', 'host')
        username = self.ui.config('tfs', 'user', None)
        password = self.ui.config('tfs', 'password', None)
        self.client = TfsClientService(host, username, password, columns, computedColumns)

    def findIds(self, description):
        start = 0
        ids = set()
        while True:
            m = self.bugRegex.search(description, start)
            if not m:
                break
            start = m.end()
            for id in m.group(1).split(','):
                if not id:
                    continue
                try:
                    ids.add(int(id))
                except ValueError:
                    continue
        return ids    

    def update(self, id, rev):
        wiRev, wiState = self._getWorkItemInfo(id)
        if not wiRev or not wiState:
            self.ui.write('Unable to find work item %s, no updates were made.\n' % id)
            return
        #elif self.resolve and wiState != 'Fixed':
        #    self._markAsResolved(id, wiRev, self._buildComment(rev))
        else:
            self.client.addWorkItemComment(id, wiRev, self._buildComment(rev))
        self.ui.write('Updated work item %s\n' % id)

    def _getWorkItemInfo(self, id):
        result = self.client.getWorkItem(id)
        if result:        
            revision = result['System.Rev']
            state = result['System.State']
            if revision and state:
                return int(revision), state    
        return None, None

    def _buildComment(self, rev):
        t = cmdutil.changeset_templater(self.ui, self.repo, False, None, None, False)
        template = templater.parsestring(self.template, quoted=False)
        t.use_template(template)
        self.ui.pushbuffer()
        t.show(rev)
        return escape(self.ui.popbuffer())

def hook(ui, repo, hooktype, node=None, **kwargs):
    if node is None:
        raise util.Abort(_('hook type %s does not pass in a changeset id' % hooktype))
    resolver = TfsResolver(ui, repo)
    rev = repo[node]
    ids = resolver.findIds(rev.description())
    if ids:
        for id in ids:
            resolver.update(id, rev)

    return False
